﻿#region Copyright(c) 2006 ZO, All right reserved.
// -----------------------------------------------------------------------------
// Copyright(c) 2006 ZO, All right reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   1.  Redistribution of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//   2.  Redistribution in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//   3.  The name of the author may not be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// -----------------------------------------------------------------------------
#endregion


#region Using directives

using System;
using System.Globalization;
using System.Text;
using ZO.SmartCore.Helpers;
using ZO.SmartCore.Security.Cryptography;
using ZO.SmartCore.Standards.GSSAPI;
using ArgumentNullException= ZO.SmartCore.Core.ArgumentNullException;
using ArgumentOutOfRangeException= ZO.SmartCore.Core.ArgumentOutOfRangeException;

#endregion

namespace ZO.SmartCore.Protocols.XMPP.SASL.Mechanisms
{
    /// <summary>
    /// MD5 Step Two
    /// </summary>
    public class Step2 : Step1
    {

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="Step2"/> class.
        /// </summary>
        public Step2()
        {
            this.Cnonce = GenerateCnonce();
            this.NC = 1;
        }


        /// <summary>
        /// Initializes a new instance of the <see cref="Step2"/> class.
        /// </summary>
        /// <param name="step">The step.</param>
        public Step2(Step1 step)
            : this()
        {
            if (step == null)
            {
                throw new ArgumentNullException("step");
            }

            this.Nonce = step.Nonce;
            this.Qop = step.Qop;
            this.Realm = step.Realm;
            this.Charset = step.Charset;
            this.Algorithm = step.Algorithm;

        }

        #endregion

        #region Destructor

        #endregion

        #region Fields
        private string _Cnonce;
        private int _NC;

        private string _AuthzID;

        private ServiceName _ServiceType;

        #endregion

        #region Events

        #endregion

        #region Operators

        #endregion

        #region Properties
        /// <summary>
        /// Indicates the type of service, such as "www" for web service,
        /// "ftp" for FTP service, "smtp" for mail delivery service, etc. 
        /// </summary>
        public ServiceName ServiceType
        {
            get { return _ServiceType; }
            set { _ServiceType = value; }
        }

        /// <summary>
        /// A client-specified data string which MUST be different each time a
        /// digest-response is sent as part of initial authentication. The
        /// cnonce-value is an opaque quoted string value provided by the
        /// client and used by both client and server to avoid chosen
        /// plaintext attacks, and to provide mutual authentication. The
        /// security of the implementation depends on a good choice. It is
        /// RECOMMENDED that it contain at least 64 bits of entropy. This
        /// directive is required and MUST be present exactly once; otherwise,
        /// authentication fails.
        /// </summary>
        public string Cnonce
        {
            get { return _Cnonce; }
            private set { _Cnonce = value; }
        }


        /// <summary>
        /// The nc-value is the hexadecimal count of the number of requests
        /// (including the current request) that the client has sent with the
        /// nonce value in this request.  For example, in the first request
        /// sent in response to a given nonce value, the client sends
        /// "nc=00000001". The purpose of this directive is to allow the
        /// server to detect request replays by maintaining its own copy of
        /// this count - if the same nc-value is seen twice, then the request
        /// is a replay.   See the description below of the construction of
        /// the response value. This directive may appear at most once; if
        /// multiple instances are present, the client should abort the
        /// authentication exchange.
        /// </summary>
        public int NC
        {
            get { return _NC; }
            set
            {
                if (value <= 0)
                {
                    throw new ArgumentOutOfRangeException("value");
                }
                this._NC = value;
            }
        }

        /// <summary>
        /// Indicates the principal name of the service with which the client
        /// wishes to connect, formed from the serv-type, host, and serv-name.
        /// For example, the FTP service on "ftp.example.com" would have a
        /// "digest-uri" value of "ftp/ftp.example.com"; the SMTP server from
        /// the example above would have a "digest-uri" value of
        /// "smtp/mail3.example.com/example.com".
        /// </summary>
        public string DigestURI
        {
            get { return ReflectionHelper.GetDescription(this.ServiceType) + "/" + this.Server; }
        }

        /// <summary>
        /// A string of 32 hex digits computed as defined below, which proves
        /// that the user knows a password. This directive is required and
        /// MUST be present exactly once; otherwise, authentication fails.
        /// </summary>
        public string Response
        {
            get { return this.GenerateResponse(); }
        }

        /// <summary>
        /// The "authorization ID" as per RFC 2222, encoded in UTF-8. This
        /// directive is optional. If present, and the authenticating user has
        /// sufficient privilege, and the server supports it, then after
        /// authentication the server will use this identity for making all
        /// accesses and access checks. If the client specifies it, and the
        /// server does not support it, then the response-value will be
        /// incorrect, and authentication will fail.
        /// </summary>
        public string AuthzID
        {
            get { return _AuthzID; }
            set { _AuthzID = value; }
        }

        #endregion

        #region Methods
        /// <summary>
        /// Generates the cnonce.
        /// </summary>
        private static string GenerateCnonce()
        {
            // Lenght of the Session ID on bytes,
            // 32 bytes equaly 64 chars
            // 16^64 possibilites for the session IDs (4.294.967.296)
            // This should be unique enough
            int length = 32;

            byte[] buffer = ByteHelper.GetRandomBytes(length);

            return ByteHelper.GetHexStringFromBytes(buffer).ToLower(CultureInfo.CurrentCulture);

        }

        private static string GenerateNC(int num)
        {
            return num.ToString(CultureInfo.CurrentCulture).PadLeft(8, '0');
        }


        /// <summary>
        /// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
        /// </returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("username=");
            sb.Append(AddQuotes(UserName));
            sb.Append(",");
            sb.Append("realm=");
            sb.Append(AddQuotes(Realm));
            sb.Append(",");
            sb.Append("nonce=");
            sb.Append(AddQuotes(Nonce));
            sb.Append(",");
            sb.Append("cnonce=");
            sb.Append(AddQuotes(this.Cnonce));
            sb.Append(",");
            sb.Append("nc=");
            sb.Append(GenerateNC(this.NC));
            sb.Append(",");
            sb.Append("qop=");
            sb.Append(Qop);
            sb.Append(",");
            sb.Append("digest-uri=");
            sb.Append(AddQuotes(this.DigestURI));
            sb.Append(",");
            sb.Append("charset=");
            sb.Append(Charset);
            sb.Append(",");
            sb.Append("response=");
            sb.Append(this.Response);
            return sb.ToString();
        }

        /// <summary>
        /// Generates the response.
        /// </summary>
        private string GenerateResponse()
        {
            byte[] H1;
            byte[] H2;
            byte[] H3;

            string A1;
            string A2;
            string A3;

            string p1;
            string p2;

            StringBuilder buffer = new StringBuilder();

            buffer.Append(this.UserName);
            buffer.Append(":");
            buffer.Append(this.Realm);
            buffer.Append(":");
            buffer.Append(this.Password);

            H1 = Cryptographer.CreateHash(HashAlgorithm.MD5, Encoding.UTF8.GetBytes(buffer.ToString()));

            buffer.Remove(0, buffer.Length);

            buffer.Append(":");
            buffer.Append(this.Nonce);
            buffer.Append(":");
            buffer.Append(this.Cnonce);

            if (String.IsNullOrEmpty(AuthzID))
            {
                buffer.Append(":");
                buffer.Append(this.AuthzID);
            }

            A1 = buffer.ToString();

            byte[] bA1 = Encoding.ASCII.GetBytes(A1);

            byte[] bH1A1 = ByteHelper.Combine(H1, bA1);

            H1 = Cryptographer.CreateHash(HashAlgorithm.MD5, bH1A1);

            buffer.Remove(0, buffer.Length);

            buffer.Append("AUTHENTICATE:");

            buffer.Append(this.DigestURI);

            if (this.Qop != QopOptions.Auth)
            {
                buffer.Append(":00000000000000000000000000000000");
            }

            A2 = buffer.ToString();

            H2 = Encoding.ASCII.GetBytes(A2);

            // create p1 and p2 as the hex representation of H1 and H2
            p1 = ByteHelper.GetHexStringFromBytes(H1);
            p2 = ByteHelper.GetHexStringFromBytes(H2);

            buffer.Remove(0, buffer.Length);

            buffer.Append(p1);
            buffer.Append(":");
            buffer.Append(this.Nonce);
            buffer.Append(":");
            buffer.Append(GenerateNC(this.NC));
            buffer.Append(":");
            buffer.Append(this.Cnonce);
            buffer.Append(":");
            buffer.Append(ReflectionHelper.GetDescription(this.Qop));
            buffer.Append(":");
            buffer.Append(p2);

            A3 = buffer.ToString();

            H3 = Cryptographer.CreateHash(HashAlgorithm.MD5, Encoding.ASCII.GetBytes(A3));

            return ByteHelper.GetHexStringFromBytes(H3);
        } // GenerateResponse

        /// <summary>
        /// return the given string with quotes
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string AddQuotes(string s)
        {
            const string quote = "\"";
            return quote + s + quote;
        }
        #endregion
    }
}
